支付宝超级 App 的弹性动态架构实践
The following article is from mPaaS Author 重岳
| 导语
本文基于重岳在 2019 年 DevOps 国际峰会北京站的分享内容进行总结,希望通过本篇文章介绍近些年来支付宝面向超大业务体量的挑战,在移动端构建弹性动态架构部分做了怎样的实战与思考,期冀能给读者们带来些许帮助。
同时,关于 mPaaS 五大组件能力,目前已正式开放试用(文末有申请地址),欢迎体验。
支付宝作为国民级应用,当前国内年活跃用户已经超过 8.7 亿,提供了超过 200 项以上的服务,而崩溃率始终维持在万分之五以下,而且每天支付宝都上线新的功能和改进。做到今天这样的成绩,并不容易,是经过长时间的实践经验积累下来的。
支付宝的架构演进主要经历了三个阶段,如果用比喻的话,可以分为独木舟、战列舰和航空母舰三个阶段。
1
独木舟时代
支付宝刚推出移动端时,它的结构非常之简单,除了一些工具组件被划分为模块,业务代码都是糅合在一起。刚开始并没有太大问题,但是当我们的研发人员迅速增长时,问题开始变得棘手起来,仅仅举几个例子便可见一斑。
研发同学晚上提交的可以运行的代码,到第二天早上来更新一下就完全不能用,原因是其他不相干团队提交代码覆盖或者污染了自己的代码。
临近发布点的时候,通常是最忙的,但不是忙着赶功能,而是忙着解决合并代码产生的各种问题,不仅浪费时间,还耽误测试同学的宝贵时间。
即使最后勉强发布了,稳定性和性能也是非常糟糕的,因为各个模块只管自己的,没有统一的规范,也缺乏统一的监控。
最令 Android 开发头痛的是 65535 的问题,彼时 Google 还没有推出 multi-dex 的方案。
这些严重的问题让我们的产品研发迭代变得无法持续下去,因此我们决定来一次彻底的重构,于是步入了战列舰时代。
2
战列舰时代
当设计新一代的客户端架构时,我们从三个方向进行思考:团队协作、研发效率、性能与稳定。
团队协作方面,我们希望整个架构分层合理,基础层面,将通用能力下沉,为更多的上层业务服务,避免重复创造轮子;业务层面,各个业务团队能够独立开发管理,不会对不相关的业务造成影响。基于这个初衷,我们形成了下图这样的架构:
整个客户端架构总共分成四层:业务层、服务层、组件层、框架层。
业务层:只需专注于业务逻辑与界面的实现,当需要调用如支付这样的通用能力时,研发同学直接使用下层提供的服务能力,不需自己开发,如此能够保证核心能力有收口,方便监控。
服务层:常用模块,如登录、支付、营销等,它们不仅自己是业务,也向其他业务提供自己的服务,我们将此类模块归类到服务层。
组件层:这一层提供的是客户端通用能力,如安全、网络、多媒体、存储这些,它们提供稳定的接口给上层使用者,同时不断优化自身内部的性能和稳定性,作为客户端的基石,它们至关重要。
框架层:最为关键的部分,包括容器、微应用、服务框架以及 Pipeline,客户端的微应用化、启动管理都依赖框架层的运作。
我们将服务层、组件层和框架层合称为 mPaaS,即移动端上的 PaaS 服务。这些 PaaS 服务可以复用,我们不仅在支付宝里使用它们,也在其他集团应用,如蚂蚁财富、网商银行等中使用。
| 业务分治
要实现业务分治,最好的方式就在代码上能够进行隔离,大家不必在同一个 Codebase 中开发,避免代码合并冲突的现象。这个通常在 Android 上通常可以通过 aar 的方式来实现,但是可惜的是我们重构的时候 aar 还没出来,而且即使有 aar,也存在打包时间随代码体积增大线性增长的问题。
我们的解决方案借鉴 OSGi 的概念,将整个客户端以 Bundle 为单位划分,每个 Bundle 可以包含自己的代码、页面和资源。读者可能会想,这究竟和 aar 有什么分别呢?其实区别很大!
首先,Bundle 里的代码部分是已编译的 dex,当编译 apk 时,我们只需要合并 dex 即可,不需要像 aar 那样将 class 编译成 dex 再进行合并,这样大大节省了打包时间;其次,Bundle 是可以独立运行于自己的 ClassLoader 中的,并且我们可以通过壳代理的方式加载 Activity 等基础组件,使得动态下发业务成为可能;最后,Bundle 里还包含微应用、服务和 Pipeline 相关的配置信息,框架会根据这些信息启动相应的组件。
mPaaS 的服务,即 Service 类似于 Spring 框架中的 Service,它对外提供接口服务,而使用者不需要知道如何初始化服务的实例以及生命周期管理,这些完全由框架来托管。使用者只需要知道目标服务接口类的方法参数即可,调用时通过框架提供的 API 来获取实例。对于服务的发布者来说,他在自己的 bundle 中声明接口类以及实现接口类派生的实例类,并注册相关信息到 bundle 的 manifest 文件中。这种做法的本质思想是 Inversion of Control,减少类之间的复杂依赖,避免繁琐的初始化工作。
以依赖接口的方式进行开发,能够解除服务使用者对服务提供者的依赖,在服务提供者尚未完全开发完成时,使用者可以完全以 mock 的方式来模拟服务,而不需要修改自己的业务代码,当然,前提是双方协商好服务接口的协议。
支付宝中的页面非常多,直接启动 Activity 或者 ViewController 对我们来说远远不够,我们选择在它们上面增加 MicroApp,即微应用的概念。微应用具备唯一的应用 ID,在框架中标识自己的存在。微应用具有统一的入口,根据使用方传入的字典参数来管理 Activity 或 ViewController。这样能够带来很多好处:
只要应用 ID 和参数协议不变,使用方不需担心目标应用内部重构带来的影响,直接使用 Activity 或者 ViewController 类名造成的引用泛滥的问题不复存在。
微应用的 ID 和字典参数特性,很容易生成 URL,从而实现外部应用使用 URL 跳转应用内页面。
从数据的角度,我们可以按业务维度来统计用户行为数据。
微应用的概念不仅适用于原生页面,同样也适用于H5和小程序。注册为H5或者小程序类型的应用 ID,框架会自动将启动过程delegate给H5或者小程序容器,而使用者完全不必关心应用 ID 对应的应用类型。
综上所述,微应用化和服务接口所赋予的特性极大提高团队间协作效率,各研发小组之间的依赖更加简单,可以各行其道,更关注于自身服务的打造建设。
| 性能优化
我们一方面在架构上作出重大改变来提高研发效率,另一方面也在不断的进行性能优化,改善用户体验。我们主要从三个层面来着手:
框架层面
制定统一开发规范,业务方使用统一的线程池、存储、网络等组件,并按需进行加载,避免不必要的启动和耗时操作。
引入 Pipeline 机制,业务模块如需在应用启动时进行初始化工作,必须使用Pipeline。框架依据业务优先级确定业务初始化实际。
利用 AOP 切面,对常用路径进行耗时统计,追踪性能瓶颈。
基础指标
对于常用指标,如闪退、ANR、内存、存储、电量、流量等,进行长期追踪。我们能够明确获悉每个版本之间这些指标上的差异,并进行采样分析,定位并解决问题。
向下突破
我们不仅仅在应用层面进行优化,同时也向下探索性能提升的可能性。在这方面,我们也收获颇丰,比如在 Android 上某些系统版本,通过在启动阶段禁用 GC 的方式获得 20%~30% 的启动时间缩减;在 iOS 上,利用系统本身的 Background Fetch 机制提高进程活跃时间,实现应用秒起。
3
航母时代
随着移动支付的不断普及,面对海量的用户和业务需求,高可用、弹性动态成为支付宝客户端更为艰巨的挑战。支付宝作为集支付、金融、生活为一体的服务平台,需要能够快速稳定的发布服务和引入第三方服务,同时对于用户的反馈和诉求必须能够积极迅速的响应。
| 动态研发模式
我们在研发模式上作出改变以业务快速迭代的要求,业务逐步由原生页面向 Web 混合页面迁移。原有的研发模式能够很好的满足团队协作的要求,但是随着业务规模的不断增大,代码量相应膨胀导致安装包太大,在 iOS 上一度超过代码段上限,无法通过 AppStore 审核;另外基于集中时间点的迭代发布,通常是一个月发布一个版本,远不能满足业务的更新速度要求。相较于原生应用开发,Web 应用的优势非常明显:
只需要一套代码,Web 应用可以在 iOS 和 Android 客户端中运行,能够相对减少人员的投入。
每个用户日常使用的功能仅仅是支付宝庞大平台中的一小部分,H5应用可以做到动态下发,因此可以消除冗余的存储,降低包大小。
近些年来 React Native,Weex 等动态渲染引擎在社区非常活跃,但经过小范围的应用以及考虑到 Web 技术的不断发展以及其在业界公认的地位,我们最终还是选择 Web 技术作为动态研发模式的基础。
Web 应用迭代摆脱了客户端集中时间点发布的束缚,各业务线迭代计划变得自主可控。
| 打磨 Web 体验
尽管 Web 应用优势明显,但在移动端上的短板也是显而易见的,它提供的用户体验、性能以及能力上的限制与原生应用有相当大的差距。为了弥补这些差距,我们做了大量的改进,主要在以下几个方面:
前后端分离,我们将页面资源离线化,这样节省了资源请求消耗的时间,使得页面打开速度提升明显,解决了在网络环境较差下容易出现白屏的问题。同时,数据请求使用 Native 网络通道,可优化的空间更大,安全性更高。
差量更新,客户端更新某业务应用版本时,不需下载完整的新版本资源包,而是下载由发布平台根据客户端本地安装版本计算生成的体积更小的差量包,这样不仅能够节省带宽和流量,也提升了业务更新的速度。
推拉结合,解决业务最新版本覆盖率的问题,每次发布新版本时,业务可主动触发消息到客户端,客户端收到通知后会更新该业务应用版本。同时,客户端会定时去检查服务端是否有版本发布,这样能够保证版本发布后大多数用户在短时间内获得最新的应用。
容错补偿,客户端可能由于网络、安全或者存储权限等原因,不能使用或者及时获得离线包,这种情况我们也考虑进来了。我们在发布离线资源时,发布平台会自动生成对应的在线 URL 并配置到应用信息中,当客户端加载 Web 应用时发现离线包不可用,会立刻启用该 URL 加载内容,能够最大程度保证业务可用性。
Android 独立浏览器内核,Android 碎片化的问题自其诞生之初业已存在,而且目前看上去没有得以解决的迹象。不同系统、不同厂商中的浏览器内核同样存在差异,这导致层出不穷的兼容性问题令研发同学头疼不已,这也违背 Web 一统天下的愿景。为了彻底解决并掌控这些问题,我们引入了独立的UC浏览器内核并集成在应用中,这样所有的问题都集中到 UC 团队解决,变得非常可控,根据数据统计,使用 UC 浏览器内核后浏览器相关的闪退和 ANR 有明显的下降。同时,安全上出现的漏洞,我们可以在第一时间修复并发布,远比厂商升级更有效率。
Web 应用全方位监控,资源加载异常、JS 执行异常、白屏、加载耗时等性能数据会被收集上报至后台,可以及时发现异常。
| 小程序
我们不仅自身提供各种各样的服务,也需要引入第三方服务来服务更多的人群,以往我们只能引入简单的第三方 H5 页面,它们只能使用支付宝提供的少量功能,而且开发人员能力的差异导致用户体验不是很理想。小程序将支付宝的能力全面开放出来,从开发到测试皆有完整的 IDE 等工具链支持,同时 DSL 简单易用,对于第三方来说,能够快速开发上线一款体验和功能比以往更强大的小程序。
| 线上高可用保障体系
在支付宝,线上风险是每个研发人员在业务前必须厘清的事情,评估风险,预防风险,监控风险,风险应急处理方案在上线都要准备好。支付宝线上的高可用保障体系由灰度发布、实时监控、诊断定位和容灾恢复四大部分组成。
灰度发布
灰度发布是预防风险的有效手段之一,对于客户端来说,线下测试的再怎么完备也无法保证在用户环境下一切正常,直接发布至全量用户是非常危险的操作,在支付宝内部属于严重违规。我们的发布平台提供多种灰度策略,包括白名单灰度、时间窗灰度、百分比灰度、基于机型地域系统等维度的灰度。新版发布前,优先选取活跃用户和问题高发的机型进行灰度,灰度期间发现并修复问题,不断扩大灰度范围,直到闪退率、卡死率等指标符合发布标准后再进行全量的发布。
实时监控
首先,制定各种线上监控指标,包括闪退、卡死、流畅度、流量、内存、存储以及业务不可用埋点等。
其次,上报策略上闪退、卡死、业务不可用埋点等高优先指标实时上报,第一时间发现异常;数据上报使用独立的进程,确保不影响主业务逻辑;当处于业务高峰时期,比如春节红包、双 11 等大型活动时,我们能够动态调整上报策略以缓解日志服务端的压力。除了自动上传和周期性上传策略外,我们通过下发诊断指令至客户端去获取平时用不到但驻留在客户端的日志,比如 logcat 日志。
诊断定位
我们能够根据客户端上报的各种埋点日志,完整描绘出用户的操作路径,根据这些信息,我们得以尝试重现用户的问题,数据的真实性相对用户提供的信息更加可靠,能减少错误信息产生的干扰。另外,通过诊断指令上传的 logcat 日志,能够更加完整的信息,帮助我们更清晰的定位问题,因此我们通常都要求研发同学在写代码的过程中能够多输出些有用的信息。
容灾处理
当故障发生时,第一时间要求是能够止血,避免损失扩大,我们通常会预置开关到业务逻辑当中,当出现业务大面积异常或资损时,后台推送业务开关至客户端中,将业务能够临时屏蔽下线。
客户端在启动阶段发生的死锁、闪退或者主页异常超过一定阈值,会自动清理应用数据,将应用还原至初始状态,能够一部分因为数据异常导致的启动问题。
我们使用 hotpatch 技术来修复原生代码,同样 hotpatch 本身是个有风险的技术,因此也要经历灰度发布的阶段来逐步验证线上稳定性,一旦发生因 patch 引起的问题,要立刻回滚 patch。
4
试用开放
支付宝的移动架构 mPaaS 不仅仅在蚂蚁金服内部得到广泛的采用,而且其核心的组件以及背后的研发理念如今已输出包括广发银行、西安银行、华夏银行等众多金融机构,帮助他们打造更具竞争力的移动平台入口。
目前我们全面开放试用体验,欢迎扫描二维码提交申请,我们会第一时间完成审核并开通权限。
活动推荐:
mPaaS Codeday#6 成都站:终端架构持续演进——小程序 · 跨平台 · 动态化
加入技术琐话读者群讨论,请在公众号回复关键词:读者群。
往期阅读
技术琐话
以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。本号由坐馆老司机技术团队维护。